page.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. 'use client';
  2. import { use, useEffect, useState, type CSSProperties } from 'react';
  3. import { useDonationHub } from '@/hooks/useDonationHub';
  4. import { fetchApi } from '@/lib/utils/client';
  5. import { CrewRankItem } from '@/types/donation';
  6. import './style.scss';
  7. type Props = {
  8. params: Promise<{ widgetToken: string }>;
  9. searchParams: Promise<{ [key: string]: string|string[]|undefined }>;
  10. };
  11. type CrewStyleConfig = {
  12. id: number;
  13. crewID: number;
  14. crewName: string;
  15. title: string;
  16. theme: number;
  17. period: number;
  18. maxDisplayCount: number;
  19. isShowAmount: boolean;
  20. isShowDonationCount: boolean;
  21. isShowContributionRate: boolean;
  22. isShowMemberIcon: boolean;
  23. isActive: boolean;
  24. bgColor: string;
  25. titleFontFamily: string|null;
  26. titleFontSizePx: number;
  27. titleFontColor: string;
  28. rank1FontFamily: string|null;
  29. rank1FontSizePx: number;
  30. rank1FontColor: string;
  31. rank2FontFamily: string|null;
  32. rank2FontSizePx: number;
  33. rank2FontColor: string;
  34. rank3FontFamily: string|null;
  35. rank3FontSizePx: number;
  36. rank3FontColor: string;
  37. rowFontFamily: string|null;
  38. rowFontSizePx: number;
  39. rowFontColor: string;
  40. };
  41. type CrewRankResponse = {
  42. list: CrewRankItem[];
  43. totalAmount: number;
  44. };
  45. const THEME_NAMES = ['basic', 'dark', 'minimal'];
  46. export default function CrewLeaderboardPage({ params, searchParams }: Props) {
  47. const { widgetToken } = use(params);
  48. const sp = use(searchParams);
  49. const configID = typeof sp.configID === 'string' ? parseInt(sp.configID, 10) : null;
  50. const hubUrl = process.env.NEXT_PUBLIC_API_URL + '/hubs/donation';
  51. const { crewRanking, setCrewRanking } = useDonationHub(widgetToken, hubUrl);
  52. const [styleCfg, setStyleCfg] = useState<CrewStyleConfig|null>(null);
  53. // 스타일 config 로드
  54. useEffect(() => {
  55. const qs = configID ? `?configID=${configID}` : '';
  56. fetchApi<CrewStyleConfig>(`/api/widget/crew/config/${widgetToken}${qs}`, { silent: true }).then(res => {
  57. if (res.data) {
  58. setStyleCfg(res.data);
  59. }
  60. }).catch(() => {});
  61. }, [widgetToken, configID]);
  62. // 순위 데이터 로드 (CrewMember 기반 base — 후원 없어도 0원으로 표시)
  63. useEffect(() => {
  64. const qs = configID ? `?configID=${configID}` : '';
  65. fetchApi<CrewRankResponse>(`/api/widget/crew/by-token/${widgetToken}${qs}`, { silent: true }).then(res => {
  66. if (res.data?.list) {
  67. setCrewRanking(res.data.list);
  68. }
  69. }).catch(() => {});
  70. }, [widgetToken, configID, setCrewRanking]);
  71. const themeName = styleCfg ? (THEME_NAMES[styleCfg.theme] ?? 'basic') : 'basic';
  72. const title = styleCfg?.title ?? '크루 순위';
  73. const isShowAmount = styleCfg?.isShowAmount ?? true;
  74. const isShowContributionRate = styleCfg?.isShowContributionRate ?? true;
  75. const isShowDonationCount = styleCfg?.isShowDonationCount ?? false;
  76. const isShowMemberIcon = styleCfg?.isShowMemberIcon ?? true;
  77. const bodyStyle = { '--crew-bg': styleCfg?.bgColor ?? '#1A1A2E' } as CSSProperties;
  78. const titleStyle = {
  79. '--crew-title-font': styleCfg?.titleFontFamily ?? 'inherit',
  80. '--crew-title-size': `${styleCfg?.titleFontSizePx ?? 18}px`,
  81. '--crew-title-color': styleCfg?.titleFontColor ?? '#FFFFFF'
  82. } as CSSProperties;
  83. const getFontStyle = (rank: number): CSSProperties => {
  84. if (rank === 1 && styleCfg) {
  85. return {
  86. '--row-font-family': styleCfg.rank1FontFamily ?? 'inherit',
  87. '--row-font-size': `${styleCfg.rank1FontSizePx}px`,
  88. '--row-font-color': styleCfg.rank1FontColor
  89. } as CSSProperties;
  90. }
  91. if (rank === 2 && styleCfg) {
  92. return {
  93. '--row-font-family': styleCfg.rank2FontFamily ?? 'inherit',
  94. '--row-font-size': `${styleCfg.rank2FontSizePx}px`,
  95. '--row-font-color': styleCfg.rank2FontColor
  96. } as CSSProperties;
  97. }
  98. if (rank === 3 && styleCfg) {
  99. return {
  100. '--row-font-family': styleCfg.rank3FontFamily ?? 'inherit',
  101. '--row-font-size': `${styleCfg.rank3FontSizePx}px`,
  102. '--row-font-color': styleCfg.rank3FontColor
  103. } as CSSProperties;
  104. }
  105. return {
  106. '--row-font-family': styleCfg?.rowFontFamily ?? 'inherit',
  107. '--row-font-size': `${styleCfg?.rowFontSizePx ?? 14}px`,
  108. '--row-font-color': styleCfg?.rowFontColor ?? '#FFFFFF'
  109. } as CSSProperties;
  110. };
  111. const getBadgeClass = (rank: number) => rank <= 3 ? `crew-widget__badge--${rank}` : 'crew-widget__badge--default';
  112. return (
  113. <div className={`crew-widget crew-widget--${themeName}`} style={bodyStyle}>
  114. <div className="crew-widget__title" style={titleStyle}>
  115. {title}
  116. </div>
  117. <div className="crew-widget__columns">
  118. <span className="crew-widget__col-rank">순위</span>
  119. {isShowMemberIcon && <span className="crew-widget__col-icon" />}
  120. <span className="crew-widget__col-name">이름</span>
  121. {isShowContributionRate && <span className="crew-widget__col-rate">기여도</span>}
  122. {isShowAmount && <span className="crew-widget__col-amount">후원 점수</span>}
  123. {isShowDonationCount && <span className="crew-widget__col-count">건수</span>}
  124. </div>
  125. <div className="crew-widget__list">
  126. {crewRanking.map(item => {
  127. const style = getFontStyle(item.rank);
  128. return (
  129. <div
  130. key={item.crewMemberID}
  131. className={`crew-widget__item${item.rank <= 3 ? ` crew-widget__item--${item.rank}` : ''}`}
  132. style={style}
  133. >
  134. <span className={`crew-widget__badge ${getBadgeClass(item.rank)}`}>{item.rank}</span>
  135. {isShowMemberIcon && (
  136. item.icon
  137. ? <img className="crew-widget__member-icon" src={item.icon} alt={item.nickname} />
  138. : <span className="crew-widget__member-icon crew-widget__member-icon--empty" />
  139. )}
  140. <span className="crew-widget__name">{item.nickname}</span>
  141. {isShowContributionRate && (
  142. <span className="crew-widget__rate">{item.contributionRate.toFixed(1)}%</span>
  143. )}
  144. {isShowAmount && (
  145. <span className="crew-widget__amount">{item.totalAmount.toLocaleString()}원</span>
  146. )}
  147. {isShowDonationCount && (
  148. <span className="crew-widget__count">{item.donationCount}건</span>
  149. )}
  150. </div>
  151. );
  152. })}
  153. </div>
  154. </div>
  155. );
  156. }